To provide a scriptable plugin that allows users to tailor the functioning of OpenCPN without getting into the complexity of writing a plugin.
Complete User Guide
Releases (including installers)
Github Repository
Cruisers Forum Thread
The plugin presents a console window comprising a script pane and a results pane and some buttons. You enter your JavaScript in the script pane (or load it from a file) and click on the Run button. The script is compiled into byte code and executed and any result is displayed. At its simplest, enter, say
(4+8)/3
and the result 4 is displayed. But you could also enter, say
function fib(n) {
if (n == 0) { return 0; }
if (n == 1) { return 1; }
return fib(n-1) + fib(n-2);
}
function build(n) {
var res = [];
for (i = 0; i <n; i++) {
res.push(fib(i));
}
return(res.join(' '));
}
print("Fibonacci says: ", build(20), “\n");
Which displays
Fibonacci says: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181
This illustrates how functions can be defined and called, including recursively. So we have a super calculator!
I have been developing interfaces (APIs) to OpenCPN and it is here that the fun starts. As an example, the following statement takes the supplied NMEA sentence, adds a checksum (replacing any existing one) and pushes it out over the OpenCPN connections
OCPNpushNMEA(“$OCRMB,A,0.000,L,,Yarmouth,5030.530,N,00120.030,W,
15.386,82.924,0.000,5030.530,S,00120.030,E,V");
A challenge has been how to arrange for the processing of OpenCPN events. I have developed a way of specifying a function to handle a particular event, so we can write, for example
OCPNonNMEAsentence(handleNMEA);
function handleNMEA(result){
if(result.OK){
print(“Received: “, result.value, “\n”);
}
else print(“Bad checksum\n”);
}
This listens out for the next NMEA sentence and displays it on receipt if the checksum is OK.
JavaScript supports the use of structures and methods. I have created structures to represent positions, waypoints and routes. You can write something like:
myposition = new Position(50.234, 3.231); // let’s change the longitude myposition.longitude = -1.674; print(“My position is “, myPosition.formatted, ‘\n”); // displays //My position is 50º 14.040’N 001º 40.440’W
mypos.NMEA returns the string 5014.04,N,140.44,W, which is the very odd way positions are represented in NMEA sentences.
This structure includes methods, as illustrated here:
sentence = "$OCRMB,A,0.000,L,,UK-S:Y,5030.530,N,00120.030,W,15.386,82.924,
0.000,5030.530,S,00120.030,E,V,A*69";
myPosition.NMEAdecode(sentence,1); // decodes NMEA sentence and sets
print(mypos.formatted, “\n"); // displays 50º 30.530’N 001º 20.030’W
myPosition.latest(); // sets myPosition to the latest available from OpenCPN
print("My latest position is", myPosition.formatted, "\n");
// might display My latest position is 50º 23.50’N 001º 41.234’W
As an illustrative application, I note that @arnolddemaa was having problems capturing magnetic variation from HDG sentences and inserting into RMC sentences. Here is a solution which captures the variation from HDG sentences and inserts it into any RMC sentences that do not already have the variation. It changes the RMC sender identity so that the originals can be blocked by OpenCPN filtering. It works by splitting the sentence into an array of fields. Note how it ends by setting up to process the next NMEA sentence.
// insert magnetic variation into RMC sentence
var vardegs = ""; // where we will save the latest variation
var varEW = "";
OCPNonNMEAsentence(processNMEA);
function processNMEA(result){
if (result.OK){
sentence = result.value;
switch (sentence.slice(3,6)){
case “HDG:" // capture variation
splut = sentence.split(",");
vardegs = splut[4]; varEW = splut[5];
break;
case "RMC":
splut = sentence.split(",");
if (splut[10] == "") { // if no variation already
splut[10] = vardegs; splut[11] = varEW;
splut[0] = “$JSRMC”;
}
result = splut.join(“,"); // put sentence together
OCPNpushNMEA(result);
break;
}
}
OCPNonNMEAsentence(processNMEA);
};
I have implemented APIs to handle OpenCPN messages. Different messages can be directed to message-specific functions. For example:
//request route list
routeRequest = '{"mode": "Not track"}' // JSON needed to get route
OCPNonMessageName(handleRL, “OCPN_ROUTELIST_RESPONSE”);
OCPNsendMessage("OCPN_ROUTELIST_REQUEST", routeRequest);
function handleRL(routeListJS){ //handle receipt of the route list
routeList = JSON.parse(routeListJS);
// notice how easy it is to parse the JSON into a structure
// for illustration, here we extract the GUID of the first route
firstGUID = routeList[0].GUID;
}
I have found the plugin an excellent way of probing OpenCPN functionality, particularly as I can evolve the script in the light of what I get, iteratively. Wondering what a route list looks like? This will show you:
OCPNonMessageName(handleRL, “OCPN_ROUTELIST_RESPONSE”);
OCPNsendMessage("OCPN_ROUTELIST_REQUEST", JSON.stringify({"mode": "Not track”}));
function handleRL(routeListJS){ // handle route list response
print(routeListJS, “\n”);
}
My own particular interest has been to:
I have described that need in another post here. An advantage of iNavX is that it displays navigational information, including estimated times en-route, in a way optimized for iPad or small phone screens. The requirement is that
The first step is to obtain the active route GUID. It turns out there is a bug in OpenCPN v5 whereby the returned GUID is actually the route name and not the GUID. This has been fixed in OpenCPN v5.1.605. So my script checks to see whether the route name was returned instead of the GUID and, if so, it has to fetch the full list of routes and look to see which is the active one. This extra work is avoided if the GUID is returned correctly.
I have a script that works as I require. It is too long to include in this post but it can be viewed here. This script includes many comments to aid understanding and I have left in, but commented out, some of the print statements used during its development. This shows key points in its evolution .
So far, I have only implemented the APIs needed to achieve my application above. I would like to hear of other possible applications so I can consider further APIs. The plugin is not yet ready for alpha testing but I would be interested to work with others in a pre-alpha phase to develop ideas. This pre-alpha phase will be MacOS only as I use Xcode for debugging. I propose we use a Slack workspace I set up to liaise with Mike. If you would like to join in, please contact me by private message. I anticipate developments will include:
At present, if you want to do separate tasks, you would need to combine them into a single script. I have ideas about running multiple independent scripts. I do not use SignalK but note its potential. I am interested in input from SignalK users to keep developments SignalK friendly.
Now to catch up on other things neglected over the past six weeks!
My JavaScript plugin is now available as an alpha release v0.1. The user guide is available here and an install package for MacOS can be downloaded from here.
I hope for feedback from users and am willing to implement further APIs to extend the interoperability with OpenCPN according to your interests.
I am Apple-based only, so am looking for volunteers to work with me on building the plugin for other platforms, especially Windows. As the present build uses Mac Xcode, someone with both Xcode and Windows would be ideal but we could probably manage if you are familiar with Cmake and the plugin development environment.
At present, if you want to do separate tasks, you would need to combine them into a single script. I have ideas about running multiple independent scripts.
I do not use SignalK but note its potential. I am interested in input from SignalK users to keep developments SignalK friendly.